/*!
* Globalize
*
* http://github.com/jquery/globalize
*
* Copyright Software Freedom Conservancy, Inc.
* Dual licensed under the MIT or GPL Version 2 licenses.
* http://jquery.org/license
*/

(function (window, undefined) {

	var Globalize,
	// private variables
	regexHex,
	regexInfinity,
	regexParseFloat,
	regexTrim,
	// private JavaScript utility functions
	arrayIndexOf,
	endsWith,
	extend,
	isArray,
	isFunction,
	isObject,
	startsWith,
	trim,
	truncate,
	zeroPad,
	// private Globalization utility functions
	appendPreOrPostMatch,
	expandFormat,
	formatDate,
	formatNumber,
	getTokenRegExp,
	getEra,
	getEraYear,
	parseExact,
	parseNegativePattern;

	// Global variable (Globalize) or CommonJS module (globalize)
	Globalize = function (cultureSelector) {
		return new Globalize.prototype.init(cultureSelector);
	};

	if (typeof require !== "undefined"
	&& typeof exports !== "undefined"
	&& typeof module !== "undefined") {
		// Assume CommonJS
		module.exports = Globalize;
	} else {
		// Export as global variable
		window.Globalize = Globalize;
	}

	Globalize.cultures = {};

	Globalize.prototype = {
		constructor: Globalize,
		init: function (cultureSelector) {
			this.cultures = Globalize.cultures;
			this.cultureSelector = cultureSelector;

			return this;
		}
	};
	Globalize.prototype.init.prototype = Globalize.prototype;

	// 1.	 When defining a culture, all fields are required except the ones stated as optional.
	// 2.	 Each culture should have a ".calendars" object with at least one calendar named "standard"
	//		 which serves as the default calendar in use by that culture.
	// 3.	 Each culture should have a ".calendar" object which is the current calendar being used,
	//		 it may be dynamically changed at any time to one of the calendars in ".calendars".
	Globalize.cultures["default"] = {
		// A unique name for the culture in the form <language code>-<country/region code>
		name: "en",
		// the name of the culture in the english language
		englishName: "English",
		// the name of the culture in its own language
		nativeName: "English",
		// whether the culture uses right-to-left text
		isRTL: false,
		// "language" is used for so-called "specific" cultures.
		// For example, the culture "es-CL" means "Spanish, in Chili".
		// It represents the Spanish-speaking culture as it is in Chili,
		// which might have different formatting rules or even translations
		// than Spanish in Spain. A "neutral" culture is one that is not
		// specific to a region. For example, the culture "es" is the generic
		// Spanish culture, which may be a more generalized version of the language
		// that may or may not be what a specific culture expects.
		// For a specific culture like "es-CL", the "language" field refers to the
		// neutral, generic culture information for the language it is using.
		// This is not always a simple matter of the string before the dash.
		// For example, the "zh-Hans" culture is netural (Simplified Chinese).
		// And the "zh-SG" culture is Simplified Chinese in Singapore, whose lanugage
		// field is "zh-CHS", not "zh".
		// This field should be used to navigate from a specific culture to it's
		// more general, neutral culture. If a culture is already as general as it
		// can get, the language may refer to itself.
		language: "en",
		// numberFormat defines general number formatting rules, like the digits in
		// each grouping, the group separator, and how negative numbers are displayed.
		numberFormat: {
			// [negativePattern]
			// Note, numberFormat.pattern has no "positivePattern" unlike percent and currency,
			// but is still defined as an array for consistency with them.
			//   negativePattern: one of "(n)|-n|- n|n-|n -"
			pattern: ["-n"],
			// number of decimal places normally shown
			decimals: 2,
			// string that separates number groups, as in 1,000,000
			",": ",",
			// string that separates a number from the fractional portion, as in 1.99
			".": ".",
			// array of numbers indicating the size of each number group.
			// TODO: more detailed description and example
			groupSizes: [3],
			// symbol used for positive numbers
			"+": "+",
			// symbol used for negative numbers
			"-": "-",
			// symbol used for NaN (Not-A-Number)
			NaN: "NaN",
			// symbol used for Negative Infinity
			negativeInfinity: "-Infinity",
			// symbol used for Positive Infinity
			positiveInfinity: "Infinity",
			percent: {
				// [negativePattern, positivePattern]
				//   negativePattern: one of "-n %|-n%|-%n|%-n|%n-|n-%|n%-|-% n|n %-|% n-|% -n|n- %"
				//   positivePattern: one of "n %|n%|%n|% n"
				pattern: ["-n %", "n %"],
				// number of decimal places normally shown
				decimals: 2,
				// array of numbers indicating the size of each number group.
				// TODO: more detailed description and example
				groupSizes: [3],
				// string that separates number groups, as in 1,000,000
				",": ",",
				// string that separates a number from the fractional portion, as in 1.99
				".": ".",
				// symbol used to represent a percentage
				symbol: "%"
			},
			currency: {
				// [negativePattern, positivePattern]
				//   negativePattern: one of "($n)|-$n|$-n|$n-|(n$)|-n$|n-$|n$-|-n $|-$ n|n $-|$ n-|$ -n|n- $|($ n)|(n $)"
				//   positivePattern: one of "$n|n$|$ n|n $"
				pattern: ["($n)", "$n"],
				// number of decimal places normally shown
				decimals: 2,
				// array of numbers indicating the size of each number group.
				// TODO: more detailed description and example
				groupSizes: [3],
				// string that separates number groups, as in 1,000,000
				",": ",",
				// string that separates a number from the fractional portion, as in 1.99
				".": ".",
				// symbol used to represent currency
				symbol: "$"
			}
		},
		// calendars defines all the possible calendars used by this culture.
		// There should be at least one defined with name "standard", and is the default
		// calendar used by the culture.
		// A calendar contains information about how dates are formatted, information about
		// the calendar's eras, a standard set of the date formats,
		// translations for day and month names, and if the calendar is not based on the Gregorian
		// calendar, conversion functions to and from the Gregorian calendar.
		calendars: {
			standard: {
				// name that identifies the type of calendar this is
				name: "Gregorian_USEnglish",
				// separator of parts of a date (e.g. "/" in 11/05/1955)
				"/": "/",
				// separator of parts of a time (e.g. ":" in 05:44 PM)
				":": ":",
				// the first day of the week (0 = Sunday, 1 = Monday, etc)
				firstDay: 0,
				days: {
					// full day names
					names: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"],
					// abbreviated day names
					namesAbbr: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"],
					// shortest day names
					namesShort: ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"]
				},
				months: {
					// full month names (13 months for lunar calendards -- 13th month should be "" if not lunar)
					names: ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December", ""],
					// abbreviated month names
					namesAbbr: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", ""]
				},
				// AM and PM designators in one of these forms:
				// The usual view, and the upper and lower case versions
				//   [ standard, lowercase, uppercase ]
				// The culture does not use AM or PM (likely all standard date formats use 24 hour time)
				//   null
				AM: ["AM", "am", "AM"],
				PM: ["PM", "pm", "PM"],
				eras: [
				// eras in reverse chronological order.
				// name: the name of the era in this culture (e.g. A.D., C.E.)
				// start: when the era starts in ticks (gregorian, gmt), null if it is the earliest supported era.
				// offset: offset in years from gregorian calendar
				{
				"name": "A.D.",
				"start": null,
				"offset": 0
			}
			],
			// when a two digit year is given, it will never be parsed as a four digit
			// year greater than this year (in the appropriate era for the culture)
			// Set it as a full year (e.g. 2029) or use an offset format starting from
			// the current year: "+19" would correspond to 2029 if the current year 2010.
			twoDigitYearMax: 2029,
			// set of predefined date and time patterns used by the culture
			// these represent the format someone in this culture would expect
			// to see given the portions of the date that are shown.
			patterns: {
				// short date pattern
				d: "M/d/yyyy",
				// long date pattern
				D: "dddd, MMMM dd, yyyy",
				// short time pattern
				t: "h:mm tt",
				// long time pattern
				T: "h:mm:ss tt",
				// long date, short time pattern
				f: "dddd, MMMM dd, yyyy h:mm tt",
				// long date, long time pattern
				F: "dddd, MMMM dd, yyyy h:mm:ss tt",
				// month/day pattern
				M: "MMMM dd",
				// month/year pattern
				Y: "yyyy MMMM",
				// S is a sortable format that does not vary by culture
				S: "yyyy\u0027-\u0027MM\u0027-\u0027dd\u0027T\u0027HH\u0027:\u0027mm\u0027:\u0027ss"
			}
			// optional fields for each calendar:
			/*
			monthsGenitive:
			Same as months but used when the day preceeds the month.
			Omit if the culture has no genitive distinction in month names.
			For an explaination of genitive months, see http://blogs.msdn.com/michkap/archive/2004/12/25/332259.aspx
			convert:
			Allows for the support of non-gregorian based calendars. This convert object is used to
			to convert a date to and from a gregorian calendar date to handle parsing and formatting.
			The two functions:
			fromGregorian( date )
			Given the date as a parameter, return an array with parts [ year, month, day ]
			corresponding to the non-gregorian based year, month, and day for the calendar.
			toGregorian( year, month, day )
			Given the non-gregorian year, month, and day, return a new Date() object
			set to the corresponding date in the gregorian calendar.
			*/
		}
	},
	// For localized strings
	messages: {}
};

Globalize.cultures["default"].calendar = Globalize.cultures["default"].calendars.standard;

Globalize.cultures["en"] = Globalize.cultures["default"];

Globalize.cultureSelector = "en";

//
// private variables
//

regexHex = /^0x[a-f0-9]+$/i;
regexInfinity = /^[+-]?infinity$/i;
regexParseFloat = /^[+-]?\d*\.?\d*(e[+-]?\d+)?$/;
regexTrim = /^\s+|\s+$/g;

//
// private JavaScript utility functions
//

arrayIndexOf = function (array, item) {
	if (array.indexOf) {
		return array.indexOf(item);
	}
	for (var i = 0, length = array.length; i < length; i++) {
		if (array[i] === item) {
			return i;
		}
	}
	return -1;
};

endsWith = function (value, pattern) {
	return value.substr(value.length - pattern.length) === pattern;
};

extend = function (deep) {
	var options, name, src, copy, copyIsArray, clone,
		target = arguments[0] || {},
		i = 1,
		length = arguments.length,
		deep = false;

	// Handle a deep copy situation
	if (typeof target === "boolean") {
		deep = target;
		target = arguments[1] || {};
		// skip the boolean and the target
		i = 2;
	}

	// Handle case when target is a string or something (possible in deep copy)
	if (typeof target !== "object" && !isFunction(target)) {
		target = {};
	}

	for (; i < length; i++) {
		// Only deal with non-null/undefined values
		if ((options = arguments[i]) != null) {
			// Extend the base object
			for (name in options) {
				src = target[name];
				copy = options[name];

				// Prevent never-ending loop
				if (target === copy) {
					continue;
				}

				// Recurse if we're merging plain objects or arrays
				if (deep && copy && (isObject(copy) || (copyIsArray = isArray(copy)))) {
					if (copyIsArray) {
						copyIsArray = false;
						clone = src && isArray(src) ? src : [];

					} else {
						clone = src && isObject(src) ? src : {};
					}

					// Never move original objects, clone them
					target[name] = extend(deep, clone, copy);

					// Don't bring in undefined values
				} else if (copy !== undefined) {
					target[name] = copy;
				}
			}
		}
	}

	// Return the modified object
	return target;
};

isArray = Array.isArray || function (obj) {
	return Object.prototype.toString.call(obj) === "[object Array]";
};

isFunction = function (obj) {
	return Object.prototype.toString.call(obj) === "[object Function]";
};

isObject = function (obj) {
	return Object.prototype.toString.call(obj) === "[object Object]";
};

startsWith = function (value, pattern) {
	return value.indexOf(pattern) === 0;
};

trim = function (value) {
	return (value + "").replace(regexTrim, "");
};

truncate = function (value) {
	return isNaN(value) ? NaN : value | 0;
};

zeroPad = function (str, count, left) {
	var l;
	for (l = str.length; l < count; l += 1) {
		str = (left ? ("0" + str) : (str + "0"));
	}
	return str;
};

//
// private Globalization utility functions
//

appendPreOrPostMatch = function (preMatch, strings) {
	// appends pre- and post- token match strings while removing escaped characters.
	// Returns a single quote count which is used to determine if the token occurs
	// in a string literal.
	var quoteCount = 0,
		escaped = false;
	for (var i = 0, il = preMatch.length; i < il; i++) {
		var c = preMatch.charAt(i);
		switch (c) {
			case "\'":
				if (escaped) {
					strings.push("\'");
				}
				else {
					quoteCount++;
				}
				escaped = false;
				break;
			case "\\":
				if (escaped) {
					strings.push("\\");
				}
				escaped = !escaped;
				break;
			default:
				strings.push(c);
				escaped = false;
				break;
		}
	}
	return quoteCount;
};

expandFormat = function (cal, format) {
	// expands unspecified or single character date formats into the full pattern.
	format = format || "F";
	var pattern,
		patterns = cal.patterns,
		len = format.length;
	if (len === 1) {
		pattern = patterns[format];
		if (!pattern) {
			throw "Invalid date format string \'" + format + "\'.";
		}
		format = pattern;
	}
	else if (len === 2 && format.charAt(0) === "%") {
		// %X escape format -- intended as a custom format string that is only one character, not a built-in format.
		format = format.charAt(1);
	}
	return format;
};

formatDate = function (value, format, culture) {
	var cal = culture.calendar,
		convert = cal.convert;

	if (!format || !format.length || format === "i") {
		var ret;
		if (culture && culture.name.length) {
			if (convert) {
				// non-gregorian calendar, so we cannot use built-in toLocaleString()
				ret = formatDate(value, cal.patterns.F, culture);
			}
			else {
				var eraDate = new Date(value.getTime()),
					era = getEra(value, cal.eras);
				eraDate.setFullYear(getEraYear(value, cal, era));
				ret = eraDate.toLocaleString();
			}
		}
		else {
			ret = value.toString();
		}
		return ret;
	}

	var eras = cal.eras,
		sortable = format === "s";
	format = expandFormat(cal, format);

	// Start with an empty string
	ret = [];
	var hour,
		zeros = ["0", "00", "000"],
		foundDay,
		checkedDay,
		dayPartRegExp = /([^d]|^)(d|dd)([^d]|$)/g,
		quoteCount = 0,
		tokenRegExp = getTokenRegExp(),
		converted;

	function padZeros(num, c) {
		var r, s = num + "";
		if (c > 1 && s.length < c) {
			r = (zeros[c - 2] + s);
			return r.substr(r.length - c, c);
		}
		else {
			r = s;
		}
		return r;
	}

	function hasDay() {
		if (foundDay || checkedDay) {
			return foundDay;
		}
		foundDay = dayPartRegExp.test(format);
		checkedDay = true;
		return foundDay;
	}

	function getPart(date, part) {
		if (converted) {
			return converted[part];
		}
		switch (part) {
			case 0: return date.getFullYear();
			case 1: return date.getMonth();
			case 2: return date.getDate();
		}
	}

	if (!sortable && convert) {
		converted = convert.fromGregorian(value);
	}

	for (; ; ) {
		// Save the current index
		var index = tokenRegExp.lastIndex,
		// Look for the next pattern
			ar = tokenRegExp.exec(format);

		// Append the text before the pattern (or the end of the string if not found)
		var preMatch = format.slice(index, ar ? ar.index : format.length);
		quoteCount += appendPreOrPostMatch(preMatch, ret);

		if (!ar) {
			break;
		}

		// do not replace any matches that occur inside a string literal.
		if (quoteCount % 2) {
			ret.push(ar[0]);
			continue;
		}

		var current = ar[0],
			clength = current.length;

		switch (current) {
			case "ddd":
				//Day of the week, as a three-letter abbreviation
			case "dddd":
				// Day of the week, using the full name
				var names = (clength === 3) ? cal.days.namesAbbr : cal.days.names;
				ret.push(names[value.getDay()]);
				break;
			case "d":
				// Day of month, without leading zero for single-digit days
			case "dd":
				// Day of month, with leading zero for single-digit days
				foundDay = true;
				ret.push(
					padZeros(getPart(value, 2), clength)
				);
				break;
			case "MMM":
				// Month, as a three-letter abbreviation
			case "MMMM":
				// Month, using the full name
				var part = getPart(value, 1);
				ret.push(
					(cal.monthsGenitive && hasDay())
					?
					cal.monthsGenitive[clength === 3 ? "namesAbbr" : "names"][part]
					:
					cal.months[clength === 3 ? "namesAbbr" : "names"][part]
				);
				break;
			case "M":
				// Month, as digits, with no leading zero for single-digit months
			case "MM":
				// Month, as digits, with leading zero for single-digit months
				ret.push(
					padZeros(getPart(value, 1) + 1, clength)
				);
				break;
			case "y":
				// Year, as two digits, but with no leading zero for years less than 10
			case "yy":
				// Year, as two digits, with leading zero for years less than 10
			case "yyyy":
				// Year represented by four full digits
				part = converted ? converted[0] : getEraYear(value, cal, getEra(value, eras), sortable);
				if (clength < 4) {
					part = part % 100;
				}
				ret.push(
					padZeros(part, clength)
				);
				break;
			case "h":
				// Hours with no leading zero for single-digit hours, using 12-hour clock
			case "hh":
				// Hours with leading zero for single-digit hours, using 12-hour clock
				hour = value.getHours() % 12;
				if (hour === 0) hour = 12;
				ret.push(
					padZeros(hour, clength)
				);
				break;
			case "H":
				// Hours with no leading zero for single-digit hours, using 24-hour clock
			case "HH":
				// Hours with leading zero for single-digit hours, using 24-hour clock
				ret.push(
					padZeros(value.getHours(), clength)
				);
				break;
			case "m":
				// Minutes with no leading zero for single-digit minutes
			case "mm":
				// Minutes with leading zero for single-digit minutes
				ret.push(
					padZeros(value.getMinutes(), clength)
				);
				break;
			case "s":
				// Seconds with no leading zero for single-digit seconds
			case "ss":
				// Seconds with leading zero for single-digit seconds
				ret.push(
					padZeros(value.getSeconds(), clength)
				);
				break;
			case "t":
				// One character am/pm indicator ("a" or "p")
			case "tt":
				// Multicharacter am/pm indicator
				part = value.getHours() < 12 ? (cal.AM ? cal.AM[0] : " ") : (cal.PM ? cal.PM[0] : " ");
				ret.push(clength === 1 ? part.charAt(0) : part);
				break;
			case "f":
				// Deciseconds
			case "ff":
				// Centiseconds
			case "fff":
				// Milliseconds
				ret.push(
					padZeros(value.getMilliseconds(), 3).substr(0, clength)
				);
				break;
			case "z":
				// Time zone offset, no leading zero
			case "zz":
				// Time zone offset with leading zero
				hour = value.getTimezoneOffset() / 60;
				ret.push(
					(hour <= 0 ? "+" : "-") + padZeros(Math.floor(Math.abs(hour)), clength)
				);
				break;
			case "zzz":
				// Time zone offset with leading zero
				hour = value.getTimezoneOffset() / 60;
				ret.push(
					(hour <= 0 ? "+" : "-") + padZeros(Math.floor(Math.abs(hour)), 2)
				// Hard coded ":" separator, rather than using cal.TimeSeparator
				// Repeated here for consistency, plus ":" was already assumed in date parsing.
					+ ":" + padZeros(Math.abs(value.getTimezoneOffset() % 60), 2)
				);
				break;
			case "g":
			case "gg":
				if (cal.eras) {
					ret.push(
						cal.eras[getEra(value, eras)].name
					);
				}
				break;
			case "/":
				ret.push(cal["/"]);
				break;
			default:
				throw "Invalid date format pattern \'" + current + "\'.";
				break;
		}
	}
	return ret.join("");
};

// formatNumber
(function () {
	var expandNumber;

	expandNumber = function (number, precision, formatInfo) {
		var groupSizes = formatInfo.groupSizes,
			curSize = groupSizes[0],
			curGroupIndex = 1,
			factor = Math.pow(10, precision),
			rounded = Math.round(number * factor) / factor;

		if (!isFinite(rounded)) {
			rounded = number;
		}
		number = rounded;

		var numberString = number + "",
			right = "",
			split = numberString.split(/e/i),
			exponent = split.length > 1 ? parseInt(split[1], 10) : 0;
		numberString = split[0];
		split = numberString.split(".");
		numberString = split[0];
		right = split.length > 1 ? split[1] : "";

		var l;
		if (exponent > 0) {
			right = zeroPad(right, exponent, false);
			numberString += right.slice(0, exponent);
			right = right.substr(exponent);
		}
		else if (exponent < 0) {
			exponent = -exponent;
			numberString = zeroPad(numberString, exponent + 1);
			right = numberString.slice(-exponent, numberString.length) + right;
			numberString = numberString.slice(0, -exponent);
		}

		if (precision > 0) {
			right = formatInfo["."] +
				((right.length > precision) ? right.slice(0, precision) : zeroPad(right, precision));
		}
		else {
			right = "";
		}

		var stringIndex = numberString.length - 1,
			sep = formatInfo[","],
			ret = "";

		while (stringIndex >= 0) {
			if (curSize === 0 || curSize > stringIndex) {
				return numberString.slice(0, stringIndex + 1) + (ret.length ? (sep + ret + right) : right);
			}
			ret = numberString.slice(stringIndex - curSize + 1, stringIndex + 1) + (ret.length ? (sep + ret) : "");

			stringIndex -= curSize;

			if (curGroupIndex < groupSizes.length) {
				curSize = groupSizes[curGroupIndex];
				curGroupIndex++;
			}
		}

		return numberString.slice(0, stringIndex + 1) + sep + ret + right;
	};

	formatNumber = function (value, format, culture) {
		if (!isFinite(value)) {
			if (value === Infinity) {
				return culture.numberFormat.positiveInfinity;
			}
			if (value === -Infinity) {
				return culture.numberFormat.negativeInfinity;
			}
			return culture.numberFormat.NaN;
		}
		if (!format || format === "i") {
			return culture.name.length ? value.toLocaleString() : value.toString();
		}
		format = format || "D";

		var nf = culture.numberFormat,
			number = Math.abs(value),
			precision = -1,
			pattern;
		if (format.length > 1) precision = parseInt(format.slice(1), 10);

		var current = format.charAt(0).toUpperCase(),
			formatInfo;

		switch (current) {
			case "D":
				pattern = "n";
				number = truncate(number);
				if (precision !== -1) {
					number = zeroPad("" + number, precision, true);
				}
				if (value < 0) number = "-" + number;
				break;
			case "N":
				formatInfo = nf;
				// fall through
			case "C":
				formatInfo = formatInfo || nf.currency;
				// fall through
			case "P":
				formatInfo = formatInfo || nf.percent;
				pattern = value < 0 ? formatInfo.pattern[0] : (formatInfo.pattern[1] || "n");
				if (precision === -1) precision = formatInfo.decimals;
				number = expandNumber(number * (current === "P" ? 100 : 1), precision, formatInfo);
				break;
			default:
				throw "Bad number format specifier: " + current;
		}

		var patternParts = /n|\$|-|%/g,
			ret = "";
		for (; ; ) {
			var index = patternParts.lastIndex,
				ar = patternParts.exec(pattern);

			ret += pattern.slice(index, ar ? ar.index : pattern.length);

			if (!ar) {
				break;
			}

			switch (ar[0]) {
				case "n":
					ret += number;
					break;
				case "$":
					ret += nf.currency.symbol;
					break;
				case "-":
					// don't make 0 negative
					if (/[1-9]/.test(number)) {
						ret += nf["-"];
					}
					break;
				case "%":
					ret += nf.percent.symbol;
					break;
			}
		}

		return ret;
	};

} ());

getTokenRegExp = function () {
	// regular expression for matching date and time tokens in format strings.
	return /\/|dddd|ddd|dd|d|MMMM|MMM|MM|M|yyyy|yy|y|hh|h|HH|H|mm|m|ss|s|tt|t|fff|ff|f|zzz|zz|z|gg|g/g;
};

getEra = function (date, eras) {
	if (!eras) return 0;
	var start, ticks = date.getTime();
	for (var i = 0, l = eras.length; i < l; i++) {
		start = eras[i].start;
		if (start === null || ticks >= start) {
			return i;
		}
	}
	return 0;
};

getEraYear = function (date, cal, era, sortable) {
	var year = date.getFullYear();
	if (!sortable && cal.eras) {
		// convert normal gregorian year to era-shifted gregorian
		// year by subtracting the era offset
		year -= cal.eras[era].offset;
	}
	return year;
};

// parseExact
(function () {
	var expandYear,
		getDayIndex,
		getMonthIndex,
		getParseRegExp,
		outOfRange,
		toUpper,
		toUpperArray;

	expandYear = function (cal, year) {
		// expands 2-digit year into 4 digits.
		if (year < 100) {
			var now = new Date(),
				era = getEra(now),
				curr = getEraYear(now, cal, era),
				twoDigitYearMax = cal.twoDigitYearMax;
			twoDigitYearMax = typeof twoDigitYearMax === "string" ? new Date().getFullYear() % 100 + parseInt(twoDigitYearMax, 10) : twoDigitYearMax;
			year += curr - (curr % 100);
			if (year > twoDigitYearMax) {
				year -= 100;
			}
		}
		return year;
	};

	getDayIndex = function (cal, value, abbr) {
		var ret,
			days = cal.days,
			upperDays = cal._upperDays;
		if (!upperDays) {
			cal._upperDays = upperDays = [
				toUpperArray(days.names),
				toUpperArray(days.namesAbbr),
				toUpperArray(days.namesShort)
			];
		}
		value = toUpper(value);
		if (abbr) {
			ret = arrayIndexOf(upperDays[1], value);
			if (ret === -1) {
				ret = arrayIndexOf(upperDays[2], value);
			}
		}
		else {
			ret = arrayIndexOf(upperDays[0], value);
		}
		return ret;
	};

	getMonthIndex = function (cal, value, abbr) {
		var months = cal.months,
			monthsGen = cal.monthsGenitive || cal.months,
			upperMonths = cal._upperMonths,
			upperMonthsGen = cal._upperMonthsGen;
		if (!upperMonths) {
			cal._upperMonths = upperMonths = [
				toUpperArray(months.names),
				toUpperArray(months.namesAbbr)
			];
			cal._upperMonthsGen = upperMonthsGen = [
				toUpperArray(monthsGen.names),
				toUpperArray(monthsGen.namesAbbr)
			];
		}
		value = toUpper(value);
		var i = arrayIndexOf(abbr ? upperMonths[1] : upperMonths[0], value);
		if (i < 0) {
			i = arrayIndexOf(abbr ? upperMonthsGen[1] : upperMonthsGen[0], value);
		}
		return i;
	};

	getParseRegExp = function (cal, format) {
		// converts a format string into a regular expression with groups that
		// can be used to extract date fields from a date string.
		// check for a cached parse regex.
		var re = cal._parseRegExp;
		if (!re) {
			cal._parseRegExp = re = {};
		}
		else {
			var reFormat = re[format];
			if (reFormat) {
				return reFormat;
			}
		}

		// expand single digit formats, then escape regular expression characters.
		var expFormat = expandFormat(cal, format).replace(/([\^\$\.\*\+\?\|\[\]\(\)\{\}])/g, "\\\\$1"),
			regexp = ["^"],
			groups = [],
			index = 0,
			quoteCount = 0,
			tokenRegExp = getTokenRegExp(),
			match;

		// iterate through each date token found.
		while ((match = tokenRegExp.exec(expFormat)) !== null) {
			var preMatch = expFormat.slice(index, match.index);
			index = tokenRegExp.lastIndex;

			// don't replace any matches that occur inside a string literal.
			quoteCount += appendPreOrPostMatch(preMatch, regexp);
			if (quoteCount % 2) {
				regexp.push(match[0]);
				continue;
			}

			// add a regex group for the token.
			var m = match[0],
				len = m.length,
				add;
			switch (m) {
				case "dddd": case "ddd":
				case "MMMM": case "MMM":
				case "gg": case "g":
					add = "(\\D+)";
					break;
				case "tt": case "t":
					add = "(\\D*)";
					break;
				case "yyyy":
				case "fff":
				case "ff":
				case "f":
					add = "(\\d{" + len + "})";
					break;
				case "dd": case "d":
				case "MM": case "M":
				case "yy": case "y":
				case "HH": case "H":
				case "hh": case "h":
				case "mm": case "m":
				case "ss": case "s":
					add = "(\\d\\d?)";
					break;
				case "zzz":
					add = "([+-]?\\d\\d?:\\d{2})";
					break;
				case "zz": case "z":
					add = "([+-]?\\d\\d?)";
					break;
				case "/":
					add = "(\\" + cal["/"] + ")";
					break;
				default:
					throw "Invalid date format pattern \'" + m + "\'.";
					break;
			}
			if (add) {
				regexp.push(add);
			}
			groups.push(match[0]);
		}
		appendPreOrPostMatch(expFormat.slice(index), regexp);
		regexp.push("$");

		// allow whitespace to differ when matching formats.
		var regexpStr = regexp.join("").replace(/\s+/g, "\\s+"),
			parseRegExp = { "regExp": regexpStr, "groups": groups };

		// cache the regex for this format.
		return re[format] = parseRegExp;
	};

	outOfRange = function (value, low, high) {
		return value < low || value > high;
	};

	toUpper = function (value) {
		// "he-IL" has non-breaking space in weekday names.
		return value.split("\u00A0").join(" ").toUpperCase();
	};

	toUpperArray = function (arr) {
		var results = [];
		for (var i = 0, l = arr.length; i < l; i++) {
			results[i] = toUpper(arr[i]);
		}
		return results;
	};

	parseExact = function (value, format, culture) {
		// try to parse the date string by matching against the format string
		// while using the specified culture for date field names.
		value = trim(value);
		var cal = culture.calendar,
		// convert date formats into regular expressions with groupings.
		// use the regexp to determine the input format and extract the date fields.
			parseInfo = getParseRegExp(cal, format),
			match = new RegExp(parseInfo.regExp).exec(value);
		if (match === null) {
			return null;
		}
		// found a date format that matches the input.
		var groups = parseInfo.groups,
			era = null, year = null, month = null, date = null, weekDay = null,
			hour = 0, hourOffset, min = 0, sec = 0, msec = 0, tzMinOffset = null,
			pmHour = false;
		// iterate the format groups to extract and set the date fields.
		for (var j = 0, jl = groups.length; j < jl; j++) {
			var matchGroup = match[j + 1];
			if (matchGroup) {
				var current = groups[j],
					clength = current.length,
					matchInt = parseInt(matchGroup, 10);
				switch (current) {
					case "dd": case "d":
						// Day of month.
						date = matchInt;
						// check that date is generally in valid range, also checking overflow below.
						if (outOfRange(date, 1, 31)) return null;
						break;
					case "MMM": case "MMMM":
						month = getMonthIndex(cal, matchGroup, clength === 3);
						if (outOfRange(month, 0, 11)) return null;
						break;
					case "M": case "MM":
						// Month.
						month = matchInt - 1;
						if (outOfRange(month, 0, 11)) return null;
						break;
					case "y": case "yy":
					case "yyyy":
						year = clength < 4 ? expandYear(cal, matchInt) : matchInt;
						if (outOfRange(year, 0, 9999)) return null;
						break;
					case "h": case "hh":
						// Hours (12-hour clock).
						hour = matchInt;
						if (hour === 12) hour = 0;
						if (outOfRange(hour, 0, 11)) return null;
						break;
					case "H": case "HH":
						// Hours (24-hour clock).
						hour = matchInt;
						if (outOfRange(hour, 0, 23)) return null;
						break;
					case "m": case "mm":
						// Minutes.
						min = matchInt;
						if (outOfRange(min, 0, 59)) return null;
						break;
					case "s": case "ss":
						// Seconds.
						sec = matchInt;
						if (outOfRange(sec, 0, 59)) return null;
						break;
					case "tt": case "t":
						// AM/PM designator.
						// see if it is standard, upper, or lower case PM. If not, ensure it is at least one of
						// the AM tokens. If not, fail the parse for this format.
						pmHour = cal.PM && (matchGroup === cal.PM[0] || matchGroup === cal.PM[1] || matchGroup === cal.PM[2]);
						if (
							!pmHour && (
								!cal.AM || (matchGroup !== cal.AM[0] && matchGroup !== cal.AM[1] && matchGroup !== cal.AM[2])
							)
						) return null;
						break;
					case "f":
						// Deciseconds.
					case "ff":
						// Centiseconds.
					case "fff":
						// Milliseconds.
						msec = matchInt * Math.pow(10, 3 - clength);
						if (outOfRange(msec, 0, 999)) return null;
						break;
					case "ddd":
						// Day of week.
					case "dddd":
						// Day of week.
						weekDay = getDayIndex(cal, matchGroup, clength === 3);
						if (outOfRange(weekDay, 0, 6)) return null;
						break;
					case "zzz":
						// Time zone offset in +/- hours:min.
						var offsets = matchGroup.split(/:/);
						if (offsets.length !== 2) return null;
						hourOffset = parseInt(offsets[0], 10);
						if (outOfRange(hourOffset, -12, 13)) return null;
						var minOffset = parseInt(offsets[1], 10);
						if (outOfRange(minOffset, 0, 59)) return null;
						tzMinOffset = (hourOffset * 60) + (startsWith(matchGroup, "-") ? -minOffset : minOffset);
						break;
					case "z": case "zz":
						// Time zone offset in +/- hours.
						hourOffset = matchInt;
						if (outOfRange(hourOffset, -12, 13)) return null;
						tzMinOffset = hourOffset * 60;
						break;
					case "g": case "gg":
						var eraName = matchGroup;
						if (!eraName || !cal.eras) return null;
						eraName = trim(eraName.toLowerCase());
						for (var i = 0, l = cal.eras.length; i < l; i++) {
							if (eraName === cal.eras[i].name.toLowerCase()) {
								era = i;
								break;
							}
						}
						// could not find an era with that name
						if (era === null) return null;
						break;
				}
			}
		}
		var result = new Date(), defaultYear, convert = cal.convert;
		defaultYear = convert ? convert.fromGregorian(result)[0] : result.getFullYear();
		if (year === null) {
			year = defaultYear;
		}
		else if (cal.eras) {
			// year must be shifted to normal gregorian year
			// but not if year was not specified, its already normal gregorian
			// per the main if clause above.
			year += cal.eras[(era || 0)].offset;
		}
		// set default day and month to 1 and January, so if unspecified, these are the defaults
		// instead of the current day/month.
		if (month === null) {
			month = 0;
		}
		if (date === null) {
			date = 1;
		}
		// now have year, month, and date, but in the culture's calendar.
		// convert to gregorian if necessary
		if (convert) {
			result = convert.toGregorian(year, month, date);
			// conversion failed, must be an invalid match
			if (result === null) return null;
		}
		else {
			// have to set year, month and date together to avoid overflow based on current date.
			result.setFullYear(year, month, date);
			// check to see if date overflowed for specified month (only checked 1-31 above).
			if (result.getDate() !== date) return null;
			// invalid day of week.
			if (weekDay !== null && result.getDay() !== weekDay) {
				return null;
			}
		}
		// if pm designator token was found make sure the hours fit the 24-hour clock.
		if (pmHour && hour < 12) {
			hour += 12;
		}
		result.setHours(hour, min, sec, msec);
		if (tzMinOffset !== null) {
			// adjust timezone to utc before applying local offset.
			var adjustedMin = result.getMinutes() - (tzMinOffset + result.getTimezoneOffset());
			// Safari limits hours and minutes to the range of -127 to 127.	 We need to use setHours
			// to ensure both these fields will not exceed this range.	adjustedMin will range
			// somewhere between -1440 and 1500, so we only need to split this into hours.
			result.setHours(result.getHours() + parseInt(adjustedMin / 60, 10), adjustedMin % 60);
		}
		return result;
	};
} ());

parseNegativePattern = function (value, nf, negativePattern) {
	var neg = nf["-"],
		pos = nf["+"],
		ret;
	switch (negativePattern) {
		case "n -":
			neg = " " + neg;
			pos = " " + pos;
			// fall through
		case "n-":
			if (endsWith(value, neg)) {
				ret = ["-", value.substr(0, value.length - neg.length)];
			}
			else if (endsWith(value, pos)) {
				ret = ["+", value.substr(0, value.length - pos.length)];
			}
			break;
		case "- n":
			neg += " ";
			pos += " ";
			// fall through
		case "-n":
			if (startsWith(value, neg)) {
				ret = ["-", value.substr(neg.length)];
			}
			else if (startsWith(value, pos)) {
				ret = ["+", value.substr(pos.length)];
			}
			break;
		case "(n)":
			if (startsWith(value, "(") && endsWith(value, ")")) {
				ret = ["-", value.substr(1, value.length - 2)];
			}
			break;
	}
	return ret || ["", value];
};

//
// public instance functions
//

Globalize.prototype.findClosestCulture = function (cultureSelector) {
	return Globalize.findClosestCulture.call(this, cultureSelector);
};

Globalize.prototype.format = function (value, format, cultureSelector) {
	return Globalize.format.call(this, value, format, cultureSelector);
};

Globalize.prototype.localize = function (key, cultureSelector) {
	return Globalize.localize.call(this, key, cultureSelector);
};

Globalize.prototype.parseInt = function (value, radix, cultureSelector) {
	return Globalize.parseInt.call(this, value, radix, cultureSelector);
};

Globalize.prototype.parseFloat = function (value, radix, cultureSelector) {
	return Globalize.parseFloat.call(this, value, radix, cultureSelector);
};

Globalize.prototype.culture = function (cultureSelector) {
	return Globalize.culture.call(this, cultureSelector);
};

//
// public singleton functions
//

Globalize.addCultureInfo = function (cultureName, baseCultureName, info) {

	var base = {},
		isNew = false;

	if (typeof cultureName !== "string") {
		// cultureName argument is optional string. If not specified, assume info is first
		// and only argument. Specified info deep-extends current culture.
		info = cultureName;
		cultureName = this.culture().name;
		base = this.cultures[cultureName];
	} else if (typeof baseCultureName !== "string") {
		// baseCultureName argument is optional string. If not specified, assume info is second
		// argument. Specified info deep-extends specified culture.
		// If specified culture does not exist, create by deep-extending default
		info = baseCultureName;
		isNew = (this.cultures[cultureName] == null);
		base = this.cultures[cultureName] || this.cultures["default"];
	} else {
		// cultureName and baseCultureName specified. Assume a new culture is being created
		// by deep-extending an specified base culture
		isNew = true;
		base = this.cultures[baseCultureName];
	}

	this.cultures[cultureName] = extend(true, {},
		base,
		info
	);
	// Make the standard calendar the current culture if it's a new culture
	if (isNew) {
		this.cultures[cultureName].calendar = this.cultures[cultureName].calendars.standard;
	}
};

Globalize.findClosestCulture = function (name) {
	var match;
	if (!name) {
		return this.cultures[this.cultureSelector] || this.cultures["default"];
	}
	if (typeof name === "string") {
		name = name.split(",");
	}
	if (isArray(name)) {
		var lang,
			cultures = this.cultures,
			list = name,
			i, l = list.length,
			prioritized = [];
		for (i = 0; i < l; i++) {
			name = trim(list[i]);
			var pri, parts = name.split(";");
			lang = trim(parts[0]);
			if (parts.length === 1) {
				pri = 1;
			}
			else {
				name = trim(parts[1]);
				if (name.indexOf("q=") === 0) {
					name = name.substr(2);
					pri = parseFloat(name);
					pri = isNaN(pri) ? 0 : pri;
				}
				else {
					pri = 1;
				}
			}
			prioritized.push({ lang: lang, pri: pri });
		}
		prioritized.sort(function (a, b) {
			return a.pri < b.pri ? 1 : -1;
		});

		// exact match
		for (i = 0; i < l; i++) {
			lang = prioritized[i].lang;
			match = cultures[lang];
			if (match) {
				return match;
			}
		}

		// neutral language match
		for (i = 0; i < l; i++) {
			lang = prioritized[i].lang;
			do {
				var index = lang.lastIndexOf("-");
				if (index === -1) {
					break;
				}
				// strip off the last part. e.g. en-US => en
				lang = lang.substr(0, index);
				match = cultures[lang];
				if (match) {
					return match;
				}
			}
			while (1);
		}

		// last resort: match first culture using that language
		for (i = 0; i < l; i++) {
			lang = prioritized[i].lang;
			for (var cultureKey in cultures) {
				var culture = cultures[cultureKey];
				if (culture.language == lang) {
					return culture;
				}
			}
		}
	}
	else if (typeof name === "object") {
		return name;
	}
	return match || null;
};

Globalize.format = function (value, format, cultureSelector) {
	culture = this.findClosestCulture(cultureSelector);
	if (value instanceof Date) {
		value = formatDate(value, format, culture);
	}
	else if (typeof value === "number") {
		value = formatNumber(value, format, culture);
	}
	return value;
};

Globalize.localize = function (key, cultureSelector) {
	return this.findClosestCulture(cultureSelector).messages[key] ||
		this.cultures["default"].messages[key];
};

Globalize.parseDate = function (value, formats, culture) {
	culture = this.findClosestCulture(culture);

	var date, prop, patterns;
	if (formats) {
		if (typeof formats === "string") {
			formats = [formats];
		}
		if (formats.length) {
			for (var i = 0, l = formats.length; i < l; i++) {
				var format = formats[i];
				if (format) {
					date = parseExact(value, format, culture);
					if (date) {
						break;
					}
				}
			}
		}
	} else {
		patterns = culture.calendar.patterns;
		for (prop in patterns) {
			date = parseExact(value, patterns[prop], culture);
			if (date) {
				break;
			}
		}
	}

	return date || null;
};

Globalize.parseInt = function (value, radix, cultureSelector) {
	return truncate(Globalize.parseFloat(value, radix, cultureSelector));
};

Globalize.parseFloat = function (value, radix, cultureSelector) {
	// radix argument is optional
	if (typeof radix !== "number") {
		cultureSelector = radix;
		radix = 10;
	}

	var culture = this.findClosestCulture(cultureSelector);
	var ret = NaN,
		nf = culture.numberFormat;

	if (value.indexOf(culture.numberFormat.currency.symbol) > -1) {
		// remove currency symbol
		value = value.replace(culture.numberFormat.currency.symbol, "");
		// replace decimal seperator
		value = value.replace(culture.numberFormat.currency["."], culture.numberFormat["."]);
	}

	// trim leading and trailing whitespace
	value = trim(value);

	// allow infinity or hexidecimal
	if (regexInfinity.test(value)) {
		ret = parseFloat(value);
	}
	else if (!radix && regexHex.test(value)) {
		ret = parseInt(value, 16);
	}
	else {

		// determine sign and number
		var signInfo = parseNegativePattern(value, nf, nf.pattern[0]),
			sign = signInfo[0],
			num = signInfo[1];

		// #44 - try parsing as "(n)"
		if (sign === "" && nf.pattern[0] !== "(n)") {
			signInfo = parseNegativePattern(value, nf, "(n)");
			sign = signInfo[0];
			num = signInfo[1];
		}

		// try parsing as "-n"
		if (sign === "" && nf.pattern[0] !== "-n") {
			signInfo = parseNegativePattern(value, nf, "-n");
			sign = signInfo[0];
			num = signInfo[1];
		}

		sign = sign || "+";

		// determine exponent and number
		var exponent,
			intAndFraction,
			exponentPos = num.indexOf("e");
		if (exponentPos < 0) exponentPos = num.indexOf("E");
		if (exponentPos < 0) {
			intAndFraction = num;
			exponent = null;
		}
		else {
			intAndFraction = num.substr(0, exponentPos);
			exponent = num.substr(exponentPos + 1);
		}
		// determine decimal position
		var integer,
			fraction,
			decSep = nf["."],
			decimalPos = intAndFraction.indexOf(decSep);
		if (decimalPos < 0) {
			integer = intAndFraction;
			fraction = null;
		}
		else {
			integer = intAndFraction.substr(0, decimalPos);
			fraction = intAndFraction.substr(decimalPos + decSep.length);
		}
		// handle groups (e.g. 1,000,000)
		var groupSep = nf[","];
		integer = integer.split(groupSep).join("");
		var altGroupSep = groupSep.replace(/\u00A0/g, " ");
		if (groupSep !== altGroupSep) {
			integer = integer.split(altGroupSep).join("");
		}
		// build a natively parsable number string
		var p = sign + integer;
		if (fraction !== null) {
			p += "." + fraction;
		}
		if (exponent !== null) {
			// exponent itself may have a number patternd
			var expSignInfo = parseNegativePattern(exponent, nf, "-n");
			p += "e" + (expSignInfo[0] || "+") + expSignInfo[1];
		}
		if (regexParseFloat.test(p)) {
			ret = parseFloat(p);
		}
	}
	return ret;
};

Globalize.culture = function (cultureSelector) {
	// setter
	if (typeof cultureSelector !== "undefined") {
		this.cultureSelector = cultureSelector;
	}
	// getter
	return this.findClosestCulture(cultureSelector) || this.culture["default"];
};

} (this));